/*
 * Unidata Platform
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 *
 * Commercial License
 * This version of Unidata Platform is licensed commercially and is the appropriate option for the vast majority of use cases.
 *
 * Please see the Unidata Licensing page at: https://unidata-platform.com/license/
 * For clarification or additional options, please contact: info@unidata-platform.com
 * -------
 * Disclaimer:
 * -------
 * THIS SOFTWARE IS DISTRIBUTED "AS-IS" WITHOUT ANY WARRANTIES, CONDITIONS AND
 * REPRESENTATIONS WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE
 * IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, MERCHANTABLE QUALITY,
 * FITNESS FOR A PARTICULAR PURPOSE, DURABILITY, NON-INFRINGEMENT, PERFORMANCE AND
 * THOSE ARISING BY STATUTE OR FROM CUSTOM OR USAGE OF TRADE OR COURSE OF DEALING.
 */
/**
 *
 */
package org.unidata.mdm.rest.search.converter;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.unidata.mdm.rest.search.ro.SearchFormFieldRO;
import org.unidata.mdm.rest.search.ro.SearchFormFieldsGroupRO;
import org.unidata.mdm.rest.search.ro.SearchSortFieldRO;
import org.unidata.mdm.rest.system.ro.SimpleDataType;
import org.unidata.mdm.search.type.FieldType;
import org.unidata.mdm.search.type.IndexField;
import org.unidata.mdm.search.type.form.FieldsGroup;
import org.unidata.mdm.search.type.form.FormField;
import org.unidata.mdm.search.type.form.FormField.FilteringType;
import org.unidata.mdm.search.type.form.FormField.SearchType;
import org.unidata.mdm.search.type.sort.SortField;

/**
 * @author Mikhail Mikhailov
 *
 */
public class SearchFieldsConverter {

    /**
     * Constructor.
     */
    private SearchFieldsConverter() {
        super();
    }

    /**
     * Extracts and converts data from rest search request to a list of internal sort fields objects.
     * @param fields the sort fields
     * @param useDefault use default field if empty or not
     * @param defaultField the default field to use
     * @return internal
     */
    @Nonnull
    public static Collection<SortField> convertSortFields(Collection<SearchSortFieldRO> fields, boolean useDefault, IndexField defaultField) {

        if (CollectionUtils.isEmpty(fields)) {
            return useDefault
                    ? Collections.singleton(SortField.of(defaultField, SortField.SortOrder.DESC))
                    : Collections.emptyList();
        }

        Collection<SortField> sortFields = new ArrayList<>(fields.size());
        for (SearchSortFieldRO sortFieldRO : fields) {

            boolean isString = SimpleDataType.fromValue(sortFieldRO.getType().value()) == SimpleDataType.STRING;
            SortField.SortOrder order = SortField.SortOrder.valueOf(sortFieldRO.getOrder());
            SortField sortField = SortField.of(sortFieldRO.getField(), isString, order);
            sortFields.add(sortField);
        }

        return sortFields;
    }

    /**
     * Conversion method.
     * @param formField - collection of form field ro
     * @param fieldSpace facet prefixes. Fields with such a prefix will be put to a separate group
     * @return collection converted form field
     */
    @Nonnull
    public static List<FieldsGroup> convertFormFields(@Nonnull Collection<SearchFormFieldRO> formField, Map<String, IndexField> fieldSpace) {

        Multimap<String, FormField> groupedFields = HashMultimap.create();
        formField.stream()
                 .map(ff -> SearchFieldsConverter.convertFormField(ff, fieldSpace))
                 .filter(Objects::nonNull)
                 .forEach(form -> groupedFields.put(form.getPath(), form));

        FieldsGroup andGroup = null;
        List<FieldsGroup> result = new ArrayList<>();
        for (String path : groupedFields.keySet()) {

            // This is not our part of the request. Just skip.
            if (!fieldSpace.containsKey(path)) {
                continue;
            }

            Collection<FormField> formFields = groupedFields.get(path);
            if (formFields.size() > 1) {
                result.add(FieldsGroup.or(formFields));
            } else if (formFields.size() == 1) {

                if (andGroup == null) {
                    andGroup = FieldsGroup.and();
                }

                formFields.forEach(andGroup::add);
            }
        }

        if (Objects.nonNull(andGroup)) {
            result.add(0, andGroup);
        }

        return result;
    }

    @Nonnull
    public static FieldsGroup convertFormFieldsGroup(@Nonnull SearchFormFieldsGroupRO source, Map<String, IndexField> fieldSpace) {

        FieldsGroup target = source.getGroupType() == SearchFormFieldsGroupRO.GroupType.AND
                ? FieldsGroup.and()
                : FieldsGroup.or();

        if(CollectionUtils.isNotEmpty(source.getFormFields())){
            target.addFields(source.getFormFields().stream()
                .map(ff -> convertFormField(ff, fieldSpace))
                .filter(Objects::nonNull)
                .collect(Collectors.toList()));
        }

        if(CollectionUtils.isNotEmpty(source.getChildGroups())){
            source.getChildGroups().forEach(childGroup -> target.add(convertFormFieldsGroup(childGroup, fieldSpace)));
        }

        return target;
    }

    /**
     * @param source source
     * @return formField
     */
    public static FormField convertFormField(@Nonnull SearchFormFieldRO source, Map<String, IndexField> fieldSpace) {

        String path = source.getPath();
        IndexField known = fieldSpace.get(path);
        if (Objects.isNull(known)) {
            return null;
        }

        FieldType type = FieldType.fromValue(source.getType().value());
        if (type != known.getFieldType()) {
            return null;
        }

        FilteringType formType = source.isInverted() ? FormField.FilteringType.NEGATIVE : FormField.FilteringType.POSITIVE;
        if (source.getRange() != null) {
            Pair<Object, Object> range = source.getRange();
            return FormField.range(known, formType, range.getLeft(), range.getRight());
        } else {
            if(source.isFuzzy()){
                return FormField.fuzzy(known, source.getSingle());
            } else if (source.isLike()) {
                return FormField.like(known, (String) source.getSingle());
            } else if (source.isStartWith()) {
                return FormField.startsWith(known, (String) source.getSingle());
            } else if (source.isMorphological()) {
                return FormField.morphological(known, (String) source.getSingle());
            } else {

                SearchType searchType = source.getSearchTypeRO() == null
                        ? SearchType.EXACT
                        : SearchType.valueOf(source.getSearchTypeRO().name());
                return FormField.of(known, formType, searchType, source.getSingle());
            }
        }
    }

    /**
     * For this method we assume, that string fields are ALWAYS analyzable.
     * This will only work for entity/classifier/rela fields and won't work for system indexes/fields.
     * @param source source
     * @return formField
     */
    @Nonnull
    public static FormField convertFormField(@Nonnull SearchFormFieldRO source) {

        FilteringType formType = source.isInverted() ? FormField.FilteringType.NEGATIVE : FormField.FilteringType.POSITIVE;
        FieldType type = FieldType.fromValue(source.getType().value());
        String path = source.getPath();
        if (source.getRange() != null) {
            Pair<Object, Object> range = source.getRange();
            return FormField.range(type, path, formType, range.getLeft(), range.getRight());
        } else {
            if(source.isFuzzy()){
                return FormField.fuzzy(type, path, type == FieldType.STRING, source.getSingle());
            } else if (source.isLike()) {
                return FormField.like(path, type == FieldType.STRING, (String) source.getSingle());
            } else if (source.isStartWith()) {
                return FormField.startsWith(path, type == FieldType.STRING, (String) source.getSingle());
            } else if (source.isMorphological()) {
                return FormField.morphological(path, type == FieldType.STRING, (String) source.getSingle());
            } else {
                return FormField.of(type, formType, source.getSearchTypeRO() == null
                        ? FormField.SearchType.EXACT
                        : FormField.SearchType.valueOf(source.getSearchTypeRO().name()), path, type == FieldType.STRING, source.getSingle());
            }
        }
    }
}
